﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using DeepZoomPivotConstructor.ErrorAndLoggingInterfaces;
using DeepZoomPivotConstructor.VisualCacheInterfaces;
using DeepZoomPivotConstructor.VisualLib;
using RenderEngineInterfaces;

namespace RenderEngine
{
    /// <summary>
    /// Main rendering engine.
    /// </summary>
    [Export(typeof(IRenderEngine))]
    public class RenderControl : IRenderEngine
    {
        [ImportMany(typeof(IRenderItem))]
        private Lazy<IRenderItem, RenderInfoAttribute>[] RenderItems = null;

#pragma warning disable 0649
        [Import(AllowDefault = true)]
        private IErrorLogger MyLogger;
#pragma warning restore 0649

        /// <summary>
        /// Create the object, set the rendering control if you wish!
        /// </summary>
        /// <param name="cache"></param>
        public RenderControl(IVisualCacheDB cache = null)
        {
            MyRenderDB = cache;
        }

        /// <summary>
        /// Renders the given object (whatever it might be).
        /// </summary>
        /// <param name="what">The object (URI, http, file, etc., can't be null however)</param>
        /// <param name="doNotUseTheseRenderPlugins">List of types we should avoid</param>
        public Visual Render(object what,
            Type[] doNotUseTheseRenderPlugins = null,
            bool forceAsComposable = false,
            bool requestComposableUnit = false)
        {
            var result = RenderToFuture(what, doNotUseTheseRenderPlugins, forceAsComposable, requestComposableUnit);
            if (result == null)
            {
                return null;
            }
            return result.Result;
        }

        /// <summary>
        /// Gets a future that will return a visual hierarchy when the
        /// rendering is done. Don't use any of the plug-ins listed for
        /// this rendering step (sub-renders might).
        /// </summary>
        /// <param name="what"></param>
        /// <param name="doNotUseTheseRenderPlugins">List of render types we should avoid - prevents recursion!</param>
        /// <param name="forceAsComposable">Set to true if we should force what is rendered to be composable unit</param>
        /// <returns></returns>
        public Task<Visual> RenderToFuture(object what, Type[] doNotUseTheseRenderPlugins = null, bool forceAsComposable = false,
            bool requestComposableUnit = false)
        {
            ///
            /// Arg checking
            /// 

            if (what == null)
            {
                throw new ArgumentException("Can't render a null thing", "what");
            }

            if (doNotUseTheseRenderPlugins == null)
            {
                doNotUseTheseRenderPlugins = new Type[0];
            }

            ///
            /// Great, next see if we can find any interfaces to render!
            /// 

            if (RenderItems == null)
            {
                if (MyLogger != null)
                {
                    MyLogger.LogMessage("RenderEngine", "No known plugins at all!");
                }
                return null;
            }

            ///
            /// Get the first one that can deal with this type
            /// and has the best priority.
            /// 

            IRenderItem theRenderItem = null;
            lock (RenderItems)
            {
                Type objectType = what.GetType();
                List<Type> badTypeList = new List<Type>(doNotUseTheseRenderPlugins);
                var possiblesWithPriority = from r in RenderItems
                                            where (r.Metadata.RenderObjectType == objectType) && (!badTypeList.Contains(r.Value.GetType()))
                                            select new
                                            {
                                                Priority = PriorityCheck(r, what),
                                                Renderer = r
                                            };

                var possibles = from r in possiblesWithPriority
                                where r.Priority != RenderPluginPriority.RPPCantDoIt
                                orderby r.Priority descending
                                select r.Renderer;

                var goodItem = possibles.FirstOrDefault();
                if (goodItem == null)
                {
                    if (MyLogger != null)
                    {
                        MyLogger.LogMessage("RenderEngine", "Unable to find a render plug-in for '" + what.ToString() + "' (type = " + what.GetType().ToString() + ").");
                    }
                    return null;
                }
                theRenderItem = goodItem.Value;
            }

            ///
            /// Now that we have the render item that we are going to use to do the rendering,
            /// lets see if there is a cache hit
            /// 

            Visual fastResult = LookupRenderInDB(what, theRenderItem);
            if (fastResult != null && fastResult.Width > 0 && fastResult.Height > 0)
            {
                return Task<Visual>.Factory.StartNew(() => { return fastResult; });
            }

            ///
            /// Generate the future and move on!
            /// 

            return Task<Visual>.Factory.StartNew(() =>
                {
                    var future = theRenderItem.Render(what);
                    if (future == null)
                    {
                        if (MyLogger != null)
                        {
                            MyLogger.LogMessage("RenderEngine", "Selected plug-in failed to render " + what.ToString() + " (plugin=" + theRenderItem.GetType().ToString() + ".");
                        }
                        return null;
                    }
                    Visual r;
                    try
                    {
                        r = future.Result;
                    }
                    catch (Exception e)
                    {
                        if (MyLogger != null)
                        {
                            MyLogger.LogMessage("RenderEngine", "Selected plug-in crashed during render " + what.ToString() + " (plugin=" + theRenderItem.GetType().ToString() + ".", e);
                        }
                        return null;
                    }
                    if (r == null)
                    {
                        return null;
                    }
                    if (r.Width <= 0 || r.Height <= 0)
                    {
                        if (MyLogger != null)
                        {
                            MyLogger.LogMessage("RenderEngine", "Selected plug-in rendered thin or short visual of " + what.ToString() + "(plugin=" + theRenderItem.GetType().ToString() + ").");
                        }
                        return null;
                    }

                    //
                    // See if it can be made composable or not. If we can't, then later on we will
                    // always assume that it is up-to-date and never gets modified. The logic is a bit
                    // strange - we use a set of heuristics... the only bomb is if we are trying to force and
                    // the image just isn't going to go.
                    //
                    // Otherwise, make it a CU if:
                    //  - The visual can be a CU
                    //  - There is no other CU inside this visual.
                    //

                    if ((forceAsComposable | requestComposableUnit) && !r.IsCompositionUnit)
                    {
                        if (forceAsComposable || !CUInside(r))
                        {
                            IRenderableObjectCompare cmp = theRenderItem as IRenderableObjectCompare;
                            if (cmp != null)
                            {
                                r.SetAsComposableUnit(cmp.GetObjectDateMetaData(what));
                            }
                            else
                            {
                                r.SetAsComposableUnit(null);
                            }
                        }
                    }
                    return ProcessFinishedVisual(r, what, theRenderItem);
                });
        }

        /// <summary>
        /// Scan a visual to see if there are any composable units inside it.
        /// </summary>
        /// <param name="r"></param>
        /// <returns></returns>
        private bool CUInside(Visual r)
        {
            return r.GetHierarchicalEnumerator(v => v.IsCompositionUnit).Any(v => v.IsCompositionUnit);
        }

        /// <summary>
        /// Returns the priority that a particular render guy will give a object.
        /// In particular, if the renderPriority throws, it logs it and returns
        /// don't know!
        /// </summary>
        /// <param name="r"></param>
        /// <param name="what"></param>
        /// <returns></returns>
        private RenderPluginPriority PriorityCheck(Lazy<IRenderItem, RenderInfoAttribute> r, object what)
        {
            try
            {
                return r.Value.RenderPriority(what);
            }
            catch (Exception)
            {
                return RenderPluginPriority.RPPCantDoIt;
            }
        }

        /// <summary>
        /// Holds onto previously rendered objects.
        /// </summary>
        private IVisualCacheDB MyRenderDB { get; set; }

        /// <summary>
        /// Lookuup the object in the render cache. If it is there, then we need to determine if
        /// that object was already rendered. If so, then return a special type of Visual that
        /// represnets that object for the DZComposer tool to use later on.
        /// </summary>
        /// <param name="what"></param>
        /// <returns></returns>
        private Visual LookupRenderInDB(object what, IRenderItem theRenderItem)
        {
            if (MyRenderDB == null)
            {
                return null;
            }

            //
            // Figure out what the hash might be for this object
            //

            long hash = 0;
            IRenderableObjectCompare theRenderCompare = theRenderItem as IRenderableObjectCompare;
            if (theRenderCompare == null)
            {
                hash = what.ToString().GetHashCode();
            }
            else
            {
                hash = theRenderCompare.GetObjectHash(what);
            }

            ///
            /// Use the db rendering guy to find a list of possible matches
            /// 

            var theOne = MyRenderDB.FindHit(hash);
            if (theOne == null)
            {
                return null;
            }

            ///
            /// We have an equal object. Now, lets make sure that whatever was used to
            /// render it last time is the same as is supposed to render it this time.
            /// 

            if (theOne.ComposeInfo.RenderItemFullType != theRenderItem.GetType().ToString())
            {
                return null;
            }

            ///
            /// Check to see if the DZ output xml file and directory actually exist!
            /// 

            if (!theOne.DZXMLFile.Exists)
            {
                return null;
            }
            DirectoryInfo dzDir = new DirectoryInfo(Path.ChangeExtension(theOne.DZXMLFile.FullName, null) + "_files");
            if (!dzDir.Exists)
            {
                return null;
            }

            ///
            /// Check to see if it out of date (or something)
            /// 

            if (theRenderCompare != null && theRenderCompare.IsOutOfDate(what, theOne.ComposeInfo.MetaData))
            {
                return null;
            }

            ///
            /// Ok - we have it! Cached! Now we can try to create a special
            /// Visual that will be "pasted over" when we do the rendering.
            /// 

            VisualItemPlaceHolder vh = new VisualItemPlaceHolder(theOne.ComposeInfo.Width, theOne.ComposeInfo.Height, theOne.DZXMLFile, theOne.Metadata, what);
            return vh;
        }

        /// <summary>
        /// The visual has been made. Do any required book-keeping.
        /// </summary>
        /// <param name="visual"></param>
        /// <returns></returns>
        private Visual ProcessFinishedVisual(Visual visual, object renderObjectRequest, IRenderItem renderItem)
        {
            ///
            /// If the visual is marked as a compositional unit, then we need
            /// to carry along the various settings for later processing.
            /// If this Visual has already been filled in, then don't re-fill it!
            /// 

            if (visual != null && visual.IsCompositionUnit && visual.CompositionInfo.RenderObject == null)
            {
                ///
                /// Make sure that we are strong enough to actually be able to
                /// run a composable unit here!
                /// 

                string renderItemType = renderItem.GetType().ToString();
                IRenderableObjectCompare compare = renderItem as IRenderableObjectCompare;

                ///
                /// Save the info
                /// 

                visual.CompositionInfo.RenderObject = renderObjectRequest;
                visual.CompositionInfo.RenderItemFullType = renderItemType;

                var bb = visual.VisualBoundingBox;
                visual.CompositionInfo.Height = bb.Height;
                visual.CompositionInfo.Width = bb.Width;

                //
                // If this is marked as composable, but there is no compare object, then we just go on.
                //

                if (compare == null)
                {
                    visual.CompositionInfo.Hash = renderObjectRequest.ToString().GetHashCode();
                }
                else
                {
                    visual.CompositionInfo.Hash = compare.GetObjectHash(renderObjectRequest);
                }
            }

            ///
            /// Done. Give it back to the person that wanted it!
            /// 

            return visual;
        }
    }
}
